/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mapbox.ohos.gestures;


import com.mapbox.ohos.gestures.ohan.PointF;

import ohos.agp.utils.Point;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;
import ohos.global.resource.NotExistException;
import ohos.global.resource.WrongTypeException;
import ohos.multimodalinput.event.TouchEvent;

import java.io.IOException;
import java.util.*;
import java.util.logging.Logger;

/**
 * Base class for all multi finger gesture detectors.
 *
 * @param <Listener> listener that will be called with gesture events/updates.
 */
public abstract class MultiFingerGesture<Listener> extends BaseGesture<Listener> {
    /**
     * This value is the threshold ratio between the previous combined pressure
     * and the current combined pressure. When pressure decreases rapidly
     * between events the position values can often be imprecise, as it usually
     * indicates that the user is in the process of lifting a pointer off of the
     * device. This value was tuned experimentally.
     * <p>
     * Thanks to Almer Thie (code.almeros.com).
     */
    private static final float PRESSURE_THRESHOLD = 0.67f;

    private static final int DEFAULT_REQUIRED_FINGERS_COUNT = 2;

    private float spanThreshold;

    private final PermittedActionsGuard permittedActionsGuard = new PermittedActionsGuard();

    /**
     * A list that holds IDs of currently active pointers in an order of activation.
     * First element is the oldest active pointer and last element is the most recently activated pointer.
     */
    final List<Integer> pointerIdList = new ArrayList<>();
    final HashMap<PointerDistancePair, MultiFingerDistancesObject> pointersDistanceMap = new HashMap<>();
    private PointF focalPoint = new PointF();

    private Point displayMetrics = new Point();

    public MultiFingerGesture(Context context, OhosGesturesManager gesturesManager) {
        super(context, gesturesManager);
        queryDisplayMetrics();
    }

    @Override
    protected boolean analyzeEvent(TouchEvent motionEvent) {
        int action = motionEvent.getAction();

        if (action == TouchEvent.PRIMARY_POINT_DOWN) {
            // As orientation can change, query the metrics in touch down
            queryDisplayMetrics();
        }

        boolean isMissingEvents =
            permittedActionsGuard.isMissingActions(action, motionEvent.getPointerCount(), pointerIdList.size())
                || (action == TouchEvent.HOVER_POINTER_MOVE && isMissingPointers(motionEvent));

        if (isMissingEvents) {
            // stopping ProgressiveGestures and clearing pointers
            if (this instanceof ProgressiveGesture && ((ProgressiveGesture) this).isInProgress()) {
                ((ProgressiveGesture) this).gestureStopped();
            }
            pointerIdList.clear();
            pointersDistanceMap.clear();
        }

        if (!isMissingEvents || action == TouchEvent.PRIMARY_POINT_DOWN) {
            // if we are not missing any actions or the invalid one happens
            // to be ACTION_DOWN (therefore, we can start over immediately), then update pointers
            updatePointerList(motionEvent);
        }

        focalPoint = Utils.determineFocalPoint(motionEvent);

        if (isMissingEvents) {
            Logger.getGlobal().warning("MultiFingerGesture ： Some MotionEvents were not passed to the library "
                + "or events from different view trees are merged.");
            return false;
        } else {
            if (action == TouchEvent.POINT_MOVE) {
                if (pointerIdList.size() >= getRequiredPointersCount()) {
                    calculateDistances();
                    if (!isSloppyGesture()) {
                        return analyzeMovement();
                    }
                }
            }
        }
        return false;
    }

    private void queryDisplayMetrics() {
        if (windowManager != null) {
            Optional<Display> display = DisplayManager.getInstance().getDefaultDisplay(context);
            display.get().getSize(displayMetrics);
        }
    }

    private void updatePointerList(TouchEvent motionEvent) {
        int action = motionEvent.getAction();
        if (action == TouchEvent.PRIMARY_POINT_DOWN || action == TouchEvent.OTHER_POINT_DOWN) {
            pointerIdList.add(motionEvent.getPointerId(motionEvent.getIndex()));
        } else if (action == TouchEvent.PRIMARY_POINT_UP || action == TouchEvent.OTHER_POINT_UP) {
            pointerIdList.remove(Integer.valueOf(motionEvent.getPointerId(motionEvent.getIndex())));
        }
    }

    private boolean isMissingPointers(TouchEvent motionEvent) {
        for (int pointerId : pointerIdList) {
            boolean hasPointer = motionEvent.getPointerPosition(pointerId) != null;
            if (!hasPointer) {
                return true;
            }
        }
        return false;
    }

    private boolean checkSpanBelowThreshold() {
        for (MultiFingerDistancesObject distancesObject : pointersDistanceMap.values()) {
            if (distancesObject.getCurrFingersDiffXY() < spanThreshold) {
                return true;
            }
        }

        return false;
    }

    /**
     * getRequiredPointersCount
     *
     * @return int
     */
    protected int getRequiredPointersCount() {
        return DEFAULT_REQUIRED_FINGERS_COUNT;
    }

    /**
     * analyzeMovement
     *
     * @return boolean
     */
    protected boolean analyzeMovement() {
        return false;
    }

    /**
     * Check if we have a sloppy gesture. Sloppy gestures can happen if the edge
     * of the user's hand is touching the screen, for example.
     * <p>
     * Thanks to Almer Thie (code.almeros.com).
     *
     * @return true if we detect sloppy gesture, false otherwise
     */
    protected boolean isSloppyGesture() {
        float rightSlopEdge = displayMetrics.getPointX();
        float bottomSlopEdge = displayMetrics.getPointY();
        try {
            for (int pointerId : pointerIdList) {
                int pointerIndex = getCurrentEvent().getPointerId(pointerId);
                float pointX = Utils.getRawX(getCurrentEvent(), pointerIndex);
                float pointY = Utils.getRawY(getCurrentEvent(), pointerIndex);
                boolean isSloppy = pointX > rightSlopEdge
                    || pointY > bottomSlopEdge;

                if (isSloppy) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return checkSpanBelowThreshold();
    }

    @Override
    protected boolean canExecute(int invokedGestureType) {
        return super.canExecute(invokedGestureType) && !isSloppyGesture();
    }

    /**
     * reset
     */
    protected void reset() {
    }

    private void calculateDistances() {
        pointersDistanceMap.clear();

        for (int i = 0; i < pointerIdList.size() - 1; i++) {
            for (int j = i + 1; j < pointerIdList.size(); j++) {
                int primaryPointerId = pointerIdList.get(i);
                int secondaryPointerId = pointerIdList.get(j);
                float px0 = getPreviousEvent().getPointerPosition(primaryPointerId).getX();
                float py0 = getPreviousEvent().getPointerPosition(primaryPointerId).getY();
                float px1 = getPreviousEvent().getPointerPosition(secondaryPointerId).getX();
                float py1 = getPreviousEvent().getPointerPosition(secondaryPointerId).getY();
                if (px0 < py0) {
                    px0 += 10;
                } else {
                    px1 += 10;
                }
                if (px1 < py1) {
                    py0 += 10;
                } else {
                    py1 += 10;
                }
                float prevFingersDiffX = px1 - px0;
                float prevFingersDiffY = py1 - py0;
                float cx0 = getCurrentEvent().getPointerPosition(primaryPointerId).getX();
                float cy0 = getCurrentEvent().getPointerPosition(primaryPointerId).getY();
                float cx1 = getCurrentEvent().getPointerPosition(secondaryPointerId).getX();
                float cy1 = getCurrentEvent().getPointerPosition(secondaryPointerId).getY();
                float currFingersDiffX = cx1 - cx0;
                float currFingersDiffY = cy1 - cy0;
                pointersDistanceMap.put(new PointerDistancePair(primaryPointerId, secondaryPointerId),
                    new MultiFingerDistancesObject(
                        prevFingersDiffX, prevFingersDiffY,
                        currFingersDiffX, currFingersDiffY)
                );
            }
        }
    }

    /**
     * Returns the current distance between the two pointers forming the
     * gesture in progress.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     * The order of parameters is irrelevant.
     *
     * @param firstPointerIndex one of pointers indexes
     * @param secondPointerIndex one of pointers indexes
     * @return Distance between pointers in pixels.
     * @see #pointerIdList
     */
    public float getCurrentSpan(int firstPointerIndex, int secondPointerIndex) {
        if (!verifyPointers(firstPointerIndex, secondPointerIndex)) {
            throw new NoSuchElementException("There is no such pair of pointers!");
        }

        MultiFingerDistancesObject distancesObject = pointersDistanceMap.get(
            new PointerDistancePair(pointerIdList.get(firstPointerIndex), pointerIdList.get(secondPointerIndex)));

        return distancesObject.getCurrFingersDiffXY();
    }

    /**
     * Returns the previous distance between the two pointers forming the
     * gesture in progress.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     * The order of parameters is irrelevant.
     *
     * @param firstPointerIndex one of pointers indexes
     * @param secondPointerIndex one of pointers indexes
     * @return Previous distance between pointers in pixels.
     * @see #pointerIdList
     */
    public float getPreviousSpan(int firstPointerIndex, int secondPointerIndex) {
        if (!verifyPointers(firstPointerIndex, secondPointerIndex)) {
            throw new NoSuchElementException("There is no such pair of pointers!");
        }

        MultiFingerDistancesObject distancesObject = pointersDistanceMap.get(
            new PointerDistancePair(pointerIdList.get(firstPointerIndex), pointerIdList.get(secondPointerIndex)));

        return distancesObject.getPrevFingersDiffXY();
    }

    /**
     * Returns current X distance between pointers in pixels.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     * The order of parameters is irrelevant.
     *
     * @param firstPointerIndex one of pointers indexes
     * @param secondPointerIndex one of pointers indexes
     * @return Current X distance between pointers in pixels.
     * @see #pointerIdList
     */
    public float getCurrentSpanX(int firstPointerIndex, int secondPointerIndex) {
        if (!verifyPointers(firstPointerIndex, secondPointerIndex)) {
            throw new NoSuchElementException("There is no such pair of pointers!");
        }

        MultiFingerDistancesObject distancesObject = pointersDistanceMap.get(
            new PointerDistancePair(pointerIdList.get(firstPointerIndex), pointerIdList.get(secondPointerIndex)));

        return Math.abs(distancesObject.getCurrFingersDiffX());
    }

    /**
     * Returns current Y distance between pointers in pixels.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     * The order of parameters is irrelevant.
     *
     * @param firstPointerIndex one of pointers indexes
     * @param secondPointerIndex one of pointers indexes
     * @return Current Y distance between pointers in pixels.
     * @see #pointerIdList
     */
    public float getCurrentSpanY(int firstPointerIndex, int secondPointerIndex) {
        if (!verifyPointers(firstPointerIndex, secondPointerIndex)) {
            throw new NoSuchElementException("There is no such pair of pointers!");
        }

        MultiFingerDistancesObject distancesObject = pointersDistanceMap.get(
            new PointerDistancePair(pointerIdList.get(firstPointerIndex), pointerIdList.get(secondPointerIndex)));

        return Math.abs(distancesObject.getCurrFingersDiffY());
    }

    /**
     * Returns previous X distance between pointers in pixels.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     * The order of parameters is irrelevant.
     *
     * @param firstPointerIndex one of pointers indexes
     * @param secondPointerIndex one of pointers indexes
     * @return Previous X distance between pointers in pixels.
     * @see #pointerIdList
     */
    public float getPreviousSpanX(int firstPointerIndex, int secondPointerIndex) {
        if (!verifyPointers(firstPointerIndex, secondPointerIndex)) {
            throw new NoSuchElementException("There is no such pair of pointers!");
        }

        MultiFingerDistancesObject distancesObject = pointersDistanceMap.get(
            new PointerDistancePair(pointerIdList.get(firstPointerIndex), pointerIdList.get(secondPointerIndex)));

        return Math.abs(distancesObject.getPrevFingersDiffX());
    }

    /**
     * Returns previous Y distance between pointers in pixels.
     * <p>
     * Pointers are sorted by the time they were placed on the screen until lifted up.
     * This means that index 0 will reflect the oldest added, still active pointer
     * and index ({@link #getPointersCount()} - 1) will reflect the latest added, still active pointer.
     * <p>
     * The order of parameters is irrelevant.
     *
     * @param firstPointerIndex one of pointers indexes
     * @param secondPointerIndex one of pointers indexes
     * @return Previous Y distance between pointers in pixels.
     * @see #pointerIdList
     */
    public float getPreviousSpanY(int firstPointerIndex, int secondPointerIndex) {
        if (!verifyPointers(firstPointerIndex, secondPointerIndex)) {
            throw new NoSuchElementException("There is no such pair of pointers!");
        }

        MultiFingerDistancesObject distancesObject = pointersDistanceMap.get(
            new PointerDistancePair(pointerIdList.get(firstPointerIndex), pointerIdList.get(secondPointerIndex)));

        return Math.abs(distancesObject.getPrevFingersDiffY());
    }

    private boolean verifyPointers(int firstPointerIndex, int secondPointerIndex) {
        return firstPointerIndex != secondPointerIndex && firstPointerIndex >= 0 && secondPointerIndex >= 0
            && firstPointerIndex < getPointersCount() && secondPointerIndex < getPointersCount();
    }

    /**
     * Returns the number of active pointers.
     *
     * @return number of active pointers.
     */
    public int getPointersCount() {
        return pointerIdList.size();
    }

    /**
     * Returns a center point of this gesture.
     *
     * @return center point of this gesture.
     */
    public PointF getFocalPoint() {
        return focalPoint;
    }

    /**
     * Get minimum span between any pair of finger that is required to pass motion events to this detector.
     *
     * @return minimum span
     */
    public float getSpanThreshold() {
        return spanThreshold;
    }

    /**
     * Set minimum span in pixels between any pair of finger that is required to pass motion events to this detector.
     * <p>
     * We encourage to set those values from dimens to accommodate for various screen sizes.
     *
     * @param spanThreshold minimum span
     */
    public void setSpanThreshold(float spanThreshold) {
        this.spanThreshold = spanThreshold;
    }

    /**
     * Set minimum span in dp between any pair of finger that is required to pass motion events to this detector.
     *
     * @param spanThresholdDimen minimum span
     */
    public void setSpanThresholdResource(int spanThresholdDimen) {
        try {
            float value = context.getResourceManager().getElement(spanThresholdDimen).getFloat();
            setSpanThreshold(value * 16.109f);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NotExistException e) {
            e.printStackTrace();
        } catch (WrongTypeException e) {
            e.printStackTrace();
        }
    }
}
